﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Vasuleg.UIDrive
{
    public class MemberPathNode
    {
        public readonly object Target;
        public readonly ReadOnlyCollection<MemberInfo> MemberPath;

        private Action<object, object> _setDelegate;
        private Func<object, object> _getDelegate;


        public MemberPathNode(object target, ReadOnlyCollection<MemberInfo> memberPath)
        {
            Target = target;
            MemberPath = memberPath;
        }

        public override string ToString()
        {
            return new StringBuilder()
                .Append(Target)
                .Append('.')
                .AppendJoin('.', MemberPath.Select(member => member.Name))
                .ToString();
        }

        public virtual void Set(object value)
        {
            if (_setDelegate != null)
            {
                _setDelegate(Target, value);
                return;
            }

            var targetParamExp = Expression.Parameter(typeof(object));
            var valueParamExp = Expression.Parameter(typeof(object));
            var targetConvertExp = Expression.Convert(targetParamExp, Target.GetType());
            var valueConvertExp = Expression.Convert(valueParamExp, MemberPath.Last() switch
            {
                FieldInfo fi => fi.FieldType,
                PropertyInfo pi => pi.PropertyType,
                _ => null
            } ?? typeof(object));

            var expression = MemberPath.Aggregate<MemberInfo, Expression>(targetConvertExp, Expression.MakeMemberAccess);

            expression = Expression.Assign(expression, valueConvertExp);
            _setDelegate = Expression.Lambda<Action<object, object>>(expression, targetParamExp, valueParamExp)
                .Compile();
            _setDelegate(Target, value);
        }
        
        public virtual object Get()
        {
            if (_getDelegate != null)
            {
                return _getDelegate(Target);
            }

            if (MemberPath is not {Count: > 0})
            {
                return (_getDelegate = _ => _)(Target);
            }

            var paramExp = new List<ParameterExpression>(MemberPath.Count);

            foreach (var t in MemberPath)
            {
                if (t.DeclaringType == null)
                {
                    return (_getDelegate = _ => null)(Target);
                }

                paramExp.Add(Expression.Variable(t.DeclaringType));
            }

            var defValue = Expression.Default(MemberPath.Last() switch
            {
                FieldInfo fi => fi.FieldType,
                PropertyInfo pi => pi.PropertyType,
                _ => null
            } ?? typeof(object));

            var nullExp = Expression.Constant(null);
            Expression lastExp = Expression.MakeMemberAccess(paramExp.Last(), MemberPath.Last());
            for (var i = paramExp.Count - 1; i >= 1; i--)
            {
                var memberExp = Expression.MakeMemberAccess(paramExp[i - 1], MemberPath[i - 1]);
                var assignExp = Expression.Assign(paramExp[i], memberExp);

                var testExp = Expression.ReferenceNotEqual(assignExp, nullExp);
                lastExp = Expression.Condition(testExp, lastExp, defValue);
            }

            var firstParam = Expression.Parameter(typeof(object));
            var convertExp = Expression.Convert(firstParam, paramExp[0].Type);

            var blockExp = Expression.Block(paramExp, Expression.Assign(paramExp[0], convertExp),
                Expression.Convert(lastExp, typeof(object)));
            _getDelegate = Expression.Lambda<Func<object, object>>(blockExp, firstParam).Compile();
            return _getDelegate(Target);
        }
    }
}